package com.onexeor.particlesview;

import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.multimodalinput.event.TouchEvent;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class ParticlesView extends Component implements Component.DrawTask, Component.TouchEventListener {
    /**
     * log tag
     */
    public static final String LOG_TAG = "Particles";

    /**
     * log HiLogLabel
     */
    public static HiLogLabel sHiLogLabel = new HiLogLabel(3, 0xD003A01, LOG_TAG);

    private static final int DEFAULT_ALPHA = 255;

    private EventHandler mHandler = new EventHandler(EventRunner.getMainEventRunner());
    private int[] mLineColor;

    private float mLinkingNodeDistance = 200;

    private int mLinkingLineColor = Color.WHITE.getValue();

    private float mLinkingLineWidth = 2;

    private int mNodesCount = 30;

    private float mNodesSize = 5;

    private float mNodesSpeedMax = 2f;

    private float mNodesSpeedMin = 1f;

    private int mNodesColor = Color.WHITE.getValue();

    private int mBackgroundColor = Color.BLACK.getValue();

    private float mTouchableRadius = 100;

    private List<DotItem> mItems;

    private Paint mLinePaint;

    private Path mPath;

    private float mTouchPosX;

    private float mTouchPosY;

    private boolean mTouchable;

    public ParticlesView(Context context) {
        this(context, null);
    }

    public ParticlesView(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public ParticlesView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        init(attrSet);
    }

    /**
     * set LinkingNode Distance
     *
     * @param linkingNodeDistance float
     */
    public void setLinkingNodeDistance(float linkingNodeDistance) {
        this.mLinkingNodeDistance = linkingNodeDistance;
    }

    /**
     * set LinkingLine Color
     *
     * @param linkingLineColor int
     */
    public void setLinkingLineColor(int linkingLineColor) {
        this.mLinkingLineColor = linkingLineColor;
    }

    /**
     * set LinkingLine Width
     *
     * @param linkingLineWidth float
     */
    public void setLinkingLineWidth(float linkingLineWidth) {
        this.mLinkingLineWidth = linkingLineWidth;
    }

    /**
     * set Nodes Count
     *
     * @param nodesCount int
     */
    public void setNodesCount(int nodesCount) {
        this.mNodesCount = nodesCount;
    }

    /**
     * set Nodes Size
     *
     * @param nodesSize float
     */
    public void setNodesSize(float nodesSize) {
        this.mNodesSize = nodesSize;
    }

    /**
     * set NodesSpeedMax
     *
     * @param nodesSpeedMax float
     */
    public void setNodesSpeedMax(float nodesSpeedMax) {
        this.mNodesSpeedMax = nodesSpeedMax;
    }

    /**
     * set NodesSpeedMin
     *
     * @param nodesSpeedMin float
     */
    public void setNodesSpeedMin(float nodesSpeedMin) {
        this.mNodesSpeedMin = nodesSpeedMin;
    }

    /**
     * set NodesColor
     *
     * @param nodesColor int
     */
    public void setNodesColor(int nodesColor) {
        this.mNodesColor = nodesColor;
    }

    /**
     * set BackgroundColor
     *
     * @param backgroundColor int
     */
    public void setBackgroundColor(int backgroundColor) {
        this.mBackgroundColor = backgroundColor;
    }

    /**
     * set Touchable Radius
     *
     * @param touchableRadius float
     */
    public void setTouchableRadius(float touchableRadius) {
        this.mTouchableRadius = touchableRadius;
    }

    /**
     * set Touchable
     *
     * @param touchable boolean
     */
    public void setTouchable(boolean touchable) {
        this.mTouchable = touchable;
    }

    private void init(AttrSet attrSet) {
        if (attrSet != null) {
            mLinkingNodeDistance = AttrUtils.getDimension(attrSet, "pv_linking_nodes_distance", 200);
            mLinkingLineColor = AttrUtils.getAttrSetColor(attrSet, "pv_linking_line_color", Color.WHITE);
            mLinkingLineWidth = AttrUtils.getDimension(attrSet, "pv_linking_line_width", 2);

            mNodesColor = AttrUtils.getAttrSetColor(attrSet, "pv_nodes_color", Color.WHITE);
            mNodesCount = AttrUtils.getInt(attrSet, "pv_nodes_count", 30);
            mNodesSize = AttrUtils.getDimension(attrSet, "pv_nodes_size", 5);
            mNodesSpeedMax = AttrUtils.getFloat(attrSet, "pv_nodes_speed_max", 2F);
            mNodesSpeedMin = AttrUtils.getFloat(attrSet, "pv_nodes_speed_max", 1F);

            mBackgroundColor = AttrUtils.getAttrSetColor(attrSet, "pv_background_color", Color.BLACK);

            mTouchable = AttrUtils.getBoolean(attrSet, "pv_touchable", false);
            mTouchableRadius = AttrUtils.getDimension(attrSet, "pv_touchable_radius", 100);
        }
        initDef();
    }

    private void initDef() {
        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setSubpixelAntiAlias(true);
        mLinePaint.setDither(true);
        mLinePaint.setStrokeWidth(mLinkingLineWidth);
        mLinePaint.setStyle(Paint.Style.STROKE_STYLE);
        mPath = new Path();
        mLinkingLineColor = Color.WHITE.getValue();

        mLineColor = getARGB(mLinkingLineColor);

        addDrawTask(this);
        setTouchEventListener(this);
        invalidate();
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (mTouchable) {
            switch (event.getAction()) {
                case TouchEvent.OTHER_POINT_DOWN:
                case TouchEvent.PRIMARY_POINT_DOWN:
                case TouchEvent.POINT_MOVE: {
                    mTouchPosX = event.getPointerPosition(0).getX();
                    mTouchPosY = event.getPointerPosition(0).getY();
                    return true;
                }
                case TouchEvent.PRIMARY_POINT_UP: {
                    mTouchPosX = -1;
                    mTouchPosY = -1;
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        HiLog.info(sHiLogLabel, LOG_TAG + "onDraw items:" + (mItems == null ? "null" : mItems.size()));
        if (mItems == null) {
            createDots();
        }
        // Fill background
        canvas.drawColor(mBackgroundColor, Canvas.PorterDuffMode.CLEAR);
        for (int i = 0; i < mItems.size(); i++) {
            DotItem itemI = mItems.get(i);
            // Render particles
            itemI.render(canvas);
            // Linking particles
            linkParticles(i, itemI, canvas);
            // Moving particles
            moveParticles(itemI);

            if (mTouchable) {
                repulseParticles(itemI);
            }
        }
        mHandler.postTask(this::invalidate);
    }

    // This part was steal from js code
    private void repulseParticles(DotItem dotItem) {
        HiLog.info(sHiLogLabel, LOG_TAG + "repulseParticles mTouchPosY:" + mTouchPosY + ":mTouchPosX:" + mTouchPosX);
        if (mTouchPosY > 0 && mTouchPosX > 0) {
            float dxM = dotItem.getPosition().getPointX() - mTouchPosX;
            float dyM = dotItem.getPosition().getPointY() - mTouchPosY;

            float distM = (float) Math.sqrt(dxM * dxM + dyM * dyM);

            float[] vector = new float[]{(dxM / distM), dyM / distM};
            float repulseRad = mTouchableRadius;
            int vel = 100;

            float repFactor =
                    (float)
                            Math.max(
                                    0,
                                    Math.min(
                                            50,
                                            (1 / repulseRad)
                                                    * (-1 * Math.pow(distM / repulseRad, 2) + 1)
                                                    * repulseRad
                                                    * vel));

            float[] pos =
                    new float[]{
                            dotItem.getPosition().getPointX() + vector[0] * repFactor,
                            dotItem.getPosition().getPointY() + vector[1] * repFactor
                    };

            if (pos[0] > 5 && pos[1] > 5 && pos[0] < getWidth() - 5 && pos[1] < getHeight() - 5) {
                dotItem.setPosition(new Point(pos[0], pos[1]));
            }
        }
    }

    private void moveParticles(DotItem item) {
        // move particle
        Point pointF = item.getPosition();
        float ms = item.getSpeed() / 2;

        if (item.getPosition().getPointX() + item.getSize() >= getWidth() - 5) {
            item.setVx(-item.getVx());
        } else if (item.getPosition().getPointX() - item.getSize() <= 5) {
            item.setVx(-item.getVx());
        }

        if (item.getPosition().getPointY() + item.getSize() >= getHeight() - 5) {
            item.setVy(-item.getVy());
        } else if (item.getPosition().getPointY() - item.getSize() <= 5) {
            item.setVy(-item.getVy());
        }

        pointF.modify(pointF.getPointX() + (item.getVx() * ms), pointF.getPointY() + (item.getVy() * ms));
    }

    private void linkParticles(int i, DotItem itemI, Canvas canvas) {
        for (int j = i + 1; j < mItems.size(); j++) {
            DotItem itemJ = mItems.get(j);
            float dx = itemI.getPosition().getPointX() - itemJ.getPosition().getPointX();
            float dy = itemI.getPosition().getPointY() - itemJ.getPosition().getPointY();
            double dist = Math.sqrt(dx * dx + dy * dy);
            if (dist <= mLinkingNodeDistance) {
                mPath.reset();
                mPath.moveTo(itemI.getPosition().getPointX(), itemI.getPosition().getPointY());
                mPath.lineTo(itemJ.getPosition().getPointX(), itemJ.getPosition().getPointY());
                mPath.close();

                int alpha = DEFAULT_ALPHA - (int) ((dist / mLinkingNodeDistance) * DEFAULT_ALPHA);

                if (alpha > 0) {
                    int c = Color.argb(alpha, mLineColor[1], mLineColor[2], mLineColor[3]);
                    mLinePaint.setColor(new Color(c));
                    canvas.drawPath(mPath, mLinePaint);
                }
            }
        }
    }

    private void createDots() {
        if (mNodesCount == 0) {
            return;
        }
        mItems = new ArrayList<>();

        SecureRandom rand = new SecureRandom();
        while (mItems.size() < mNodesCount) {
            float finalX = (float) (Math.random() * getWidth());
            float finalY = (float) (Math.random() * getHeight());
            float size = (float) (Math.random() * mNodesSize);
            float finalSpeed = rand.nextFloat() * (mNodesSpeedMax - mNodesSpeedMin) + mNodesSpeedMin;

            DotItem dotItem = new DotItem();
            dotItem.setSize(size == 0F ? (0.3F * mNodesSize) : size);
            dotItem.setPosition(new Point(finalX, finalY));
            dotItem.setColor(mNodesColor);
            dotItem.setSpeed(finalSpeed);
            dotItem.setVx((float) Math.random() - 0.5F);
            dotItem.setVy((float) Math.random() - 0.5F);
            mItems.add(dotItem);
        }
    }

    private int[] getARGB(int color) {
        int[] lineColor = new int[4];
        lineColor[0] = Color.alpha(color);

        RgbColor rgbColor = new RgbColor(color);
        lineColor[1] = rgbColor.getRed();
        lineColor[2] = rgbColor.getGreen();
        lineColor[3] = rgbColor.getBlue();
        return lineColor;
    }
}
